package variabilitysmelldetection.measurement.astvisitors;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.CatchClause;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.InstanceofExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;

import variabilitysmelldetection.measurement.models.ComponentItem;


public class CouplingBetweenComponentsVisitor extends ASTVisitor {
	private ComponentItem componentItem;
	private List<String> referencedTypes;
	
	public CouplingBetweenComponentsVisitor(ComponentItem componentItem) {
		this.componentItem = componentItem;
		referencedTypes = new ArrayList<String>();
	}
	
	public boolean visit(TypeDeclaration type) {
		
		@SuppressWarnings("unchecked")
		List<Type> interfaces = type.superInterfaceTypes();
		for (Type t : interfaces) {
			addType(t.toString());
		}
		
		Type superClass = type.getSuperclassType();
		if (superClass != null) {
			addType(superClass.toString());			
		}
		
		if (componentItem.isRefinement()) {
			addType(componentItem.getName());
		}
		
		return true;
	}

	@Override
	public boolean visit(FieldDeclaration field) {
		
		checkType(field.getType());
		
		return super.visit(field);
	}
	
	@Override
	public boolean visit(MethodDeclaration method) {
		//DEBUG
		//System.out.println(componentItem.getParentName()+"/"+componentItem.getName()+": "+method.getName()+" --> "+method.getReturnType2());
		checkMethodParameters(method);
		checkMethodReturnType(method);
		checkMethodThrows(method);
		checkMethodBody(method);
		
		return super.visit(method);
	} 
	
	public int getCBO() {
		return referencedTypes.size();
	}
	
	public List<String> getReferencedTypes() {
		return referencedTypes;
	}
	
	private void checkType(Type type) {
		
		if (!type.isPrimitiveType()) {
			String typeName = type.toString();
			
			if (!typeName.equals(componentItem.getName().replace(".jak", ""))) {
				
				// nao adicionar arrays de tipos primitivos
				if (type.isArrayType()) {
					ArrayType arrayType = (ArrayType)type;
					if (!arrayType.getComponentType().isPrimitiveType()) {				
						typeName = arrayType.getComponentType().toString();
					} else {
						typeName = "";
					}
				}
	
				addType(typeName);
			}		
			// DEBUG
			/*if (!typeName.equals("")) {
				System.out.println(componentItem.getParentName()+"/"+componentItem.getName()+": "+typeName);
			}*/
		}	
	}
	
	private void checkMethodReturnType(MethodDeclaration method){
		if (!method.isConstructor()) {
			checkType(method.getReturnType2());
		}
	}
	
	private void checkMethodParameters(MethodDeclaration method){
		for (Object parameter : method.parameters()) {
			VariableDeclaration variableDeclaration = (VariableDeclaration) parameter;
			Type type = (Type)variableDeclaration.getStructuralProperty(SingleVariableDeclaration.TYPE_PROPERTY);
			checkType(type);
		}
	}
	
	private void checkMethodThrows(MethodDeclaration method){
		@SuppressWarnings("unchecked")
		List<Name> throwsList = method.thrownExceptions();
		for (Name type : throwsList) {
			addType(type.toString());
		}
	}
	
	private void checkMethodBody(MethodDeclaration method){
		method.accept(new MethodVisitor());
	}
	
	private void addType(String type){
		if (!type.equals("") && !referencedTypes.contains(type.toString())) {
			referencedTypes.add(type.toString());
		}
	}
 
	private class MethodVisitor extends ASTVisitor{
		
		public boolean visit(VariableDeclarationStatement fd) {
			checkType(fd.getType());
			
			for (Object frag : fd.fragments()){
				VariableDeclarationFragment f = (VariableDeclarationFragment)frag;
				Expression init = f.getInitializer();
				if (init != null) {
					init.accept(new ASTVisitor(){
			
						/**
						 * Chamada de metodo (Classe.metodo)
						 */
						public boolean visit(MethodInvocation node) {
							checkMethodInvocationExpression(node.getExpression());							
							return super.visit(node);
						}
					});
				}
			}
			
            return false;
        }
		
		/**
		 * Acesso a atributos (Classe.atributo)
		 */
		public boolean visit(QualifiedName node) {
			String name = node.getQualifier().getFullyQualifiedName();
			if (Character.isUpperCase(name.codePointAt(0))) {
				addType(name);
			}
			return super.visit(node);
		}

		/**
		 * Chamada de metodo (Classe.metodo)
		 */
		public boolean visit(MethodInvocation node) {
			checkMethodInvocationExpression(node.getExpression());
			
			return super.visit(node);
		}
		
				
		/**
		 * Instanciacao
		 */
		public boolean visit(ClassInstanceCreation node){
			checkType(node.getType());
			return super.visit(node);
		} 
		
		/**
		 * Intanceof
		 */
		public boolean visit(InstanceofExpression node) {
			checkType(node.getRightOperand());
			return super.visit(node);
		}
		
		/**
		 * Casting
		 */
		public boolean visit(CastExpression cast){
			checkType(cast.getType());
			return super.visit(cast);
		}
		
		/**
		 * Catch
		 */
		public boolean visit(CatchClause catchC){
			checkType(catchC.getException().getType());
			return super.visit(catchC);
		}
		
		private void checkMethodInvocationExpression(Expression exp){
			
			if (exp != null) {
				String name = exp.toString();
				if (name.contains(".")) {
					name = name.substring(0, name.indexOf("."));	
				}
				
				if (!name.equals("Super()") && Character.isUpperCase(name.codePointAt(0))) {
					addType(name);
				}
			}
		}
	}
}